feat(credits): gate scheduled agent runs on credits + Telegram alert on shortfall - #782
feat(credits): gate scheduled agent runs on credits + Telegram alert on shortfall#782sweetmantech wants to merge 1 commit into
Conversation
…on shortfall `runAgentWorkflow` charged credits AFTER the turn with no pre-gate, so an unfunded account (out of credits, no card to auto-recharge) kept running scheduled turns into a deep negative balance — WAVS Digital hit -1,563 (recoupable/chat#1885). Add a pre-run credit gate that reuses the request-path gate (`ensureCreditsOrShortCircuit`, which attempts a silent auto-recharge) so scheduled runs enforce the same rules as `/api/research` and the social-scrape routes: - `gateWorkflowCredits` — a `"use step"` that checks a 1-credit floor before `runAgentStep`. On shortfall it fires a Telegram alert and returns `hasCredits:false`; the workflow skips the run before any model token is spent. Accounts with a saved card top up transparently inside the gate. Fails OPEN on a transient Stripe/Supabase error so an infra blip never breaks every run. - `alertCreditShortfall` — formats + sends the team Telegram alert, swallowing errors (alerting must not break the workflow), mirroring `sendSalesNotification`. - Wire the gate first inside `runAgentWorkflow`'s try so `finally` still runs cleanup (clear active stream, close writable, revoke ephemeral key) when a run is skipped. TDD: RED→GREEN for the alert helper, the gate step, and the workflow wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 42 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
3 issues found across 6 files
Confidence score: 2/5
- In
app/lib/workflows/runAgentWorkflow.tsandapp/lib/workflows/gateWorkflowCredits.ts, credit gating is not strong enough under concurrency and low-balance edge cases, so multiple runs can pass the check and then debit later, driving scheduled accounts negative and weakening spend controls — add an atomic reserve/check (or enforce a non-negative floor at debit time) before model execution. - In
app/lib/workflows/gateWorkflowCredits.ts, allowing a turn when balance is exactly one credit can still permit a much larger downstream debit inhandleChatCredits, which creates a concrete regression risk for credit enforcement — tighten the gate to validate against expected turn cost rather than a minimal threshold. - In
lib/credits/__tests__/alertCreditShortfall.test.ts, alert-content assertions missparse_mode: "Markdown"andsessionId, so formatting/context regressions could slip through unnoticed — extend the test expectations to include both fields.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/lib/workflows/runAgentWorkflow.ts">
<violation number="1" location="app/lib/workflows/runAgentWorkflow.ts:95">
P1: Concurrent workflows for the same account can all pass this gate against the same balance, then spend model tokens before `handleChatCredits` debits them. An atomic reservation/check in the gate, or a floor-enforcing debit RPC, would be needed so only runs backed by available credits proceed.</violation>
</file>
<file name="app/lib/workflows/gateWorkflowCredits.ts">
<violation number="1" location="app/lib/workflows/gateWorkflowCredits.ts:51">
P1: A balance of exactly one credit still allows the model turn, even though the later `handleChatCredits` debit can be much larger than one credit; that leaves the scheduled account negative and does not achieve the stated protection against unfunded runs. A preflight/reservation based on the expected turn cost or a database-side minimum-balance check would make the gate enforceable.</violation>
</file>
<file name="lib/credits/__tests__/alertCreditShortfall.test.ts">
<violation number="1" location="lib/credits/__tests__/alertCreditShortfall.test.ts:22">
P3: The `alertCreditShortfall` test that verifies the alert content doesn't assert `parse_mode: "Markdown"` or the sessionId in the message text. The Markdown parse mode is intentional — without it, the `*bold*` formatting in the alert body would render as literal asterisks rather than bold text in Telegram. Consider adding assertions for `{ parse_mode: "Markdown" }` on the second argument and for the sessionId appearing in the text to pin the full expected behavior.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as Scheduled Runner
participant API as /api/chat/runs
participant Wf as runAgentWorkflow
participant Gate as gateWorkflowCredits
participant Credits as ensureCreditsOrShortCircuit
participant Stripe
participant DB as Supabase
participant Alert as alertCreditShortfall
participant TG as Telegram Bot
participant Step as runAgentStep
participant Cleanup as finally block
Note over Client,Cleanup: Scheduled agent run flow
Client->>API: POST /api/chat/runs
API->>Wf: runAgentWorkflow(input)
Wf->>Wf: Enter try block
Note over Wf,Gate: NEW: Pre-run credit gate
Wf->>Gate: gateWorkflowCredits(accountId, chatId, sessionId)
Gate->>Gate: "use step" (DB/Stripe I/O in workflow)
Gate->>Credits: ensureCreditsOrShortCircuit(accountId, creditsToDeduct=1)
Credits->>DB: Check current balance
alt Balance < 1 AND auto-recharge available
Credits->>Stripe: Attempt silent auto-recharge
Stripe-->>Credits: Charge result
Credits->>DB: Update balance
Credits-->>Gate: null (proceed)
else Balance >= 1
Credits-->>Gate: null (proceed)
else Balance < 1 AND no auto-recharge
Credits-->>Gate: NextResponse (402)
end
alt Shortfall (NextResponse returned)
Gate->>Alert: alertCreditShortfall(accountId, chatId, sessionId)
Alert->>TG: sendMessage(formatted markdown)
Note over Alert,TG: Swallows errors - alerting never breaks workflow
TG-->>Alert: OK
Gate-->>Wf: { hasCredits: false }
Wf->>Wf: Log warning, return early
Wf->>Cleanup: finally block runs
Cleanup->>Cleanup: clearActiveStream, closeChatStream, revoke ephemeral key
else Credits available or fail-open
Gate-->>Wf: { hasCredits: true }
Note over Wf: Original flow continues unchanged
Wf->>Wf: Generate assistantMessageId
Wf->>Step: runAgentStep(...)
Step->>Credits: Post-run charge
Step-->>Wf: result
Wf->>Cleanup: finally block
end
Note over Gate: Fail-open on transient error
Note over Gate,Credits: If ensureCreditsOrShortCircuit throws
Gate->>Gate: Log error, return { hasCredits: true }
Gate-->>Wf: { hasCredits: true } (run proceeds)
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // (recoupable/chat#1885 — WAVS Digital hit -1,563). Gate first — before we | ||
| // even mint an assistant message id or spend a model token — and skip the | ||
| // run on shortfall, firing a Telegram alert. `finally` still runs cleanup. | ||
| const gate = await gateWorkflowCredits({ |
There was a problem hiding this comment.
P1: Concurrent workflows for the same account can all pass this gate against the same balance, then spend model tokens before handleChatCredits debits them. An atomic reservation/check in the gate, or a floor-enforcing debit RPC, would be needed so only runs backed by available credits proceed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/lib/workflows/runAgentWorkflow.ts, line 95:
<comment>Concurrent workflows for the same account can all pass this gate against the same balance, then spend model tokens before `handleChatCredits` debits them. An atomic reservation/check in the gate, or a floor-enforcing debit RPC, would be needed so only runs backed by available credits proceed.</comment>
<file context>
@@ -84,20 +85,41 @@ export async function runAgentWorkflow(input: RunAgentWorkflowInput): Promise<vo
+ // (recoupable/chat#1885 — WAVS Digital hit -1,563). Gate first — before we
+ // even mint an assistant message id or spend a model token — and skip the
+ // run on shortfall, firing a Telegram alert. `finally` still runs cleanup.
+ const gate = await gateWorkflowCredits({
+ accountId: input.accountId,
+ chatId: input.chatId,
</file context>
| try { | ||
| const short = await ensureCreditsOrShortCircuit({ | ||
| accountId, | ||
| creditsToDeduct: SCHEDULED_RUN_MIN_CREDITS, |
There was a problem hiding this comment.
P1: A balance of exactly one credit still allows the model turn, even though the later handleChatCredits debit can be much larger than one credit; that leaves the scheduled account negative and does not achieve the stated protection against unfunded runs. A preflight/reservation based on the expected turn cost or a database-side minimum-balance check would make the gate enforceable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/lib/workflows/gateWorkflowCredits.ts, line 51:
<comment>A balance of exactly one credit still allows the model turn, even though the later `handleChatCredits` debit can be much larger than one credit; that leaves the scheduled account negative and does not achieve the stated protection against unfunded runs. A preflight/reservation based on the expected turn cost or a database-side minimum-balance check would make the gate enforceable.</comment>
<file context>
@@ -0,0 +1,70 @@
+ try {
+ const short = await ensureCreditsOrShortCircuit({
+ accountId,
+ creditsToDeduct: SCHEDULED_RUN_MIN_CREDITS,
+ successUrl: CREDIT_AUTO_RECHARGE_FALLBACK_SUCCESS_URL,
+ });
</file context>
| expect(sendMessage).toHaveBeenCalledTimes(1); | ||
| const text = vi.mocked(sendMessage).mock.calls[0]?.[0] as string; | ||
| expect(text).toContain("acc-1"); | ||
| expect(text).toContain("chat-9"); |
There was a problem hiding this comment.
P3: The alertCreditShortfall test that verifies the alert content doesn't assert parse_mode: "Markdown" or the sessionId in the message text. The Markdown parse mode is intentional — without it, the *bold* formatting in the alert body would render as literal asterisks rather than bold text in Telegram. Consider adding assertions for { parse_mode: "Markdown" } on the second argument and for the sessionId appearing in the text to pin the full expected behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/credits/__tests__/alertCreditShortfall.test.ts, line 22:
<comment>The `alertCreditShortfall` test that verifies the alert content doesn't assert `parse_mode: "Markdown"` or the sessionId in the message text. The Markdown parse mode is intentional — without it, the `*bold*` formatting in the alert body would render as literal asterisks rather than bold text in Telegram. Consider adding assertions for `{ parse_mode: "Markdown" }` on the second argument and for the sessionId appearing in the text to pin the full expected behavior.</comment>
<file context>
@@ -0,0 +1,32 @@
+ expect(sendMessage).toHaveBeenCalledTimes(1);
+ const text = vi.mocked(sendMessage).mock.calls[0]?.[0] as string;
+ expect(text).toContain("acc-1");
+ expect(text).toContain("chat-9");
+ });
+
</file context>
What
runAgentWorkflowcharged credits after the turn with no pre-gate, so an unfunded account (out of credits, no saved card to auto-recharge) kept running scheduled turns into a deep negative balance — WAVS Digital hit −1,563 credits (recoupable/chat#1885, "Split out — billing & engagement integrity").This wires the existing request-path credit gate into the workflow so scheduled runs enforce the same rules as
/api/researchand the social-scrape routes, and alerts the team on shortfall.Changes
gateWorkflowCredits(app/lib/workflows/) — a"use step"run BEFORErunAgentStep. ReusesensureCreditsOrShortCircuit(which attempts a silent auto-recharge against a saved card) with a 1-credit floor. On shortfall it fires a Telegram alert and returnshasCredits:false; the workflow skips the run before any model token is spent. Fails open on a transient Stripe/Supabase error so an infra blip never breaks every run.alertCreditShortfall(lib/credits/) — formats + sends the team Telegram alert (account/chat/session), swallowing errors so alerting never breaks the workflow (mirrorssendSalesNotification).runAgentWorkflow— gate first, inside the existingtry, sofinallystill runs cleanup (clear active stream, close writable, revoke ephemeral key) when a run is skipped. Accounts with a saved card top up transparently and proceed unchanged.Covers both the headless
/api/chat/runspath (the scheduled burners this targets) and the interactive/api/chat/workflowpath — funded/carded accounts are unaffected either way.Scope notes
deduct_credits_with_auditis intentionally left out — it's adatabase/migration (cross-submodule) and this pre-gate already stops the drain at its source. Recommend a follow-up in the billing-integrity tracker.Verification
alertCreditShortfall— sends alert w/ account+chat; never throws on Telegram failure.gateWorkflowCredits— available →hasCredits:trueno alert; shortfall →hasCredits:false+ alert; 1-credit floor; fail-open on throw.runAgentWorkflow— gates before the step; skips step/billing/auto-commit + no id minted on block; cleanup + ephemeral-key revoke still run on block.lib/credits+app/lib/workflows— 140 tests passing.tsc --noEmit: no new errors in changed files (2 pre-existing baseline errors inrunAgentWorkflow.ts/.test.tsare unchanged, only line-shifted).eslintclean on all changed files.Targets main.
Relates to recoupable/chat#1885 (split-out billing & engagement integrity).
🤖 Generated with Claude Code
Summary by cubic
Add a pre-run credit gate to scheduled and interactive agent runs to block unfunded accounts before any tokens are used. On shortfall, we send a Telegram alert; carded accounts auto‑recharge and proceed.
runAgentStep; on shortfall it skips the run and alerts Telegram. Cleanup still runs (stream clear/close, ephemeral key revoke).ensureCreditsOrShortCircuitpath; silently tops up if a card is saved and fails open on transient Stripe/Supabase errors to avoid false blocks.Written for commit 01cef4f. Summary will update on new commits.